package jaux.tank.dao.utils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.domain.Specification;

import jaux.tank.bean.vo.EntityQueryCondition;
import jaux.tank.dao.annotation.EntityQueryParam;
import jaux.tank.dao.enums.EntityQueryParamType;
import jaux.tank.utils.StrKit;

/**
 * Jpa 查询条件工具类.
 */
public class JpaUtils {

	private static final Logger logger = LoggerFactory.getLogger(JpaUtils.class);
	
	private JpaUtils() {}

	/**
	 * 根据查询条件生成jpa查询描述.
	 *
	 * @param <T> the generic type
	 * @param conditions the conditions
	 * @param clz the clz
	 * @return the specification
	 */
	public static <T> Specification<T> genPredicateByConditions(List<EntityQueryCondition> conditions, Class<? extends T> clz) {
		return genPredicateByConditions(conditions, clz, false); // 默认为and
	}
	/**
	 * 根据查询条件生成jpa查询描述.
	 *
	 * @param <T> the generic type
	 * @param conditions the conditions
	 * @param clz the clz
	 * @return the specification
	 */
	public static <T> Specification<T> genPredicateByConditions(List<EntityQueryCondition> conditions, Class<? extends T> clz, boolean isOr) {
		List<EntityQueryParamInfo> infoLst = getQueryParamsInfos(conditions, clz);
		return genPredicateByInfos(infoLst, isOr);
	}
	/**
	 * 根据查询条件生成jpa查询描述.
	 *
	 * @param <T> the generic type
	 * @param params the params
	 * @param clz the clz
	 * @return the specification
	 */
	private static <T> Specification<T> genPredicateByInfos(List<EntityQueryParamInfo> infoLst, boolean isOr) {
		if (infoLst == null || infoLst.isEmpty())
			return null;
		return (root, query, builder) -> {
			List<Predicate> predicates = new LinkedList<>();
			Map<PluralAttribute<?,?,?>, Path<?>> paPathMap = new HashMap<>();
			for (EntityQueryParamInfo infos : infoLst) {
				Predicate predicate = genPredicateByInfos(infos,root,query,builder,paPathMap);
				if(predicate != null) {
					predicates.add(predicate);
				}
			}
			if(isOr) {
				return builder.or(predicates.toArray(new Predicate[predicates.size()]));
				
			}else {
				return builder.and(predicates.toArray(new Predicate[predicates.size()]));
				
			}
		};
	}
	/**
	 * 根据查询条件生成jpa查询描述.
	 *
	 * @param <T> the generic type
	 * @param params the params
	 * @param clz the clz
	 * @return the specification
	 */
	private static <T> Specification<T> genPredicateByInfos(List<EntityQueryParamInfo> infoLst) {
		return genPredicateByInfos(infoLst, false);
	}
	
	private static <T> Predicate genPredicateByInfos(EntityQueryParamInfo infos
			, Root<T> root
			, CriteriaQuery<?> query
			, CriteriaBuilder builder
			,Map<PluralAttribute<?,?,?>, Path<?>> paPathMap) {
		/*
		 * 根据不同匹配方式，装配查询条件
		 */
		Predicate predicate = null;
		Object val = infos.value;
		boolean valIsNotEmptyStr=infos.emptyStr;
		Collection<?> values= infos.values;
		if(StringUtils.isBlank(infos.target))
			return builder.conjunction();
		Path<?>  targetPath = null;
		boolean collectionsAttribute=false;
		
		String[] pathArr = infos.target.split("\\.");
		EntityType<T> model = root.getModel();
		for (String str : pathArr) {
			if (targetPath == null) {
				Optional<PluralAttribute<? super T, ?, ?>> paOpt = model.getPluralAttributes().stream().filter(pa->pa.getName().equals(str)).findAny();
				if(pathArr.length>1 && paOpt.isPresent()){
					collectionsAttribute = true;
					final PluralAttribute<? super T, ?, ?> pa = paOpt.get();
					if(paPathMap.get(pa)!=null) {
						targetPath = paPathMap.get(pa);
						continue;
					}
					switch(pa.getCollectionType()) {
					case COLLECTION:
						targetPath = root.join(model.getCollection(str));
						break;
					case LIST:
						targetPath = root.join(model.getList(str));
						break;
					case MAP:
						targetPath = root.join(model.getMap(str));
						break;
					case SET:
						targetPath = root.join(model.getSet(str));
						break;
					default:
						break;
					}
					paPathMap.put(pa, targetPath);
				}else {
					targetPath = root.get(str);
				}
			}else {
				targetPath = targetPath.get(str);
			}
		}
		if(targetPath == null)
			return builder.conjunction();
		final Path<?>  finalPath = targetPath;
		if(finalPath!=null 
				&& finalPath.getJavaType().isEnum() 
				&& val !=null
				&& !Arrays.asList(EntityQueryParamType.IS_NULL, EntityQueryParamType.IS_NOT_NULL).contains(infos.queryType)) {
			String valString = val.toString();
			Object[] enums = finalPath.getJavaType().getEnumConstants();
			val = Arrays.stream(enums)
					.filter(e->((Enum<?>)e).name().equals(valString))
					.findAny().orElse(null);
			if(CollectionUtils.isNotEmpty(values)) {
				values =  values.stream()
						.map(v->Arrays.stream(enums)
								.filter(e->((Enum<?>)e).name().equals( v.toString()))
								.findAny().orElse(null))
						.collect(Collectors.toList());
			}
		}
		boolean valueCanCompare = val instanceof Comparable &&( !(val instanceof String) || valIsNotEmptyStr);
		switch (infos.queryType) {
			case EQUAL:
				if (valueCanCompare) {
					predicate = builder.equal(finalPath, val);
				}
				break;
			case NOT_EQUAL:
				if (valueCanCompare) {
					predicate = builder.notEqual(finalPath, val);
				}
				break;
			case LIKE:
				predicate = builder.like(finalPath.as(String.class), "%"+val+"%");
				break;
			case NOT_LIKE:
				predicate = builder.notLike(finalPath.as(String.class), "%"+val+"%");
				break;
			case LESS:
				if (valueCanCompare) {
					predicate = builder.lessThan((Path<Comparable< Object>>)finalPath , (Comparable<Object>)val );
				}
				break;
			case LESS_OR_EQUAL:
				if (valueCanCompare) {
					predicate = builder.lessThanOrEqualTo((Path<Comparable< Object>>)finalPath , (Comparable<Object>)val );
				}
				break;
			case GREATER:
				if (valueCanCompare) {
					predicate = builder.greaterThan((Path<Comparable< Object>>)finalPath , (Comparable<Object>)val );
				}
				break;
			case GREATER_OR_EQUAL:
				if (valueCanCompare) {
					predicate = builder.greaterThanOrEqualTo((Path<Comparable< Object>>)finalPath , (Comparable<Object>)val );
				}
				break;
			case BETWEEN:
				if(val instanceof BetweenCondition<?>) {
					BetweenCondition<?> between = (BetweenCondition<?>)val;
					predicate = builder.between((Path<Comparable< Object>>)finalPath, (Comparable< Object>)between.getMin(), (Comparable< Object>)between.getMax());
				}else if(values.size()>=2) {
					
					Comparable<Object>[] minAndMax = values.toArray(new Comparable[0]);
					predicate = builder.between((Path<Comparable< Object>>)finalPath, minAndMax[0], minAndMax[minAndMax.length-1]);
					if(minAndMax.length>2) {
						logger.warn("between query only need two params,only the first and the last param will be used,{}", values);
					}
				}
				break;
			case NOT_BETWEEN:
				if(val instanceof BetweenCondition<?>) {
					BetweenCondition<?> between = (BetweenCondition<?>)val;
					predicate = builder.not(builder.between((Path<Comparable< Object>>)finalPath, (Comparable< Object>)between.getMin(), (Comparable< Object>)between.getMax()));
				}else if(values.size()>=2) {
					
					Comparable<Object>[] minAndMax = values.toArray(new Comparable[0]);
					predicate = builder.not(builder.between((Path<Comparable< Object>>)finalPath, minAndMax[0], minAndMax[minAndMax.length-1]));
					if(minAndMax.length>2) {
						logger.warn("between query only need two params,only the first and the last param will be used,{}", values);
					}
				}
				break;
			case IN:
				if(!values.isEmpty())
					predicate = finalPath.in(values);
				break;
			case NOT_IN:
				if(!values.isEmpty())
					predicate = finalPath.in(values).not();
				break;
			case IS_NULL:
				if(Boolean.TRUE.equals(val)) {
					predicate = finalPath.isNull();
				}else if(Boolean.FALSE.equals(val)) {
					predicate = finalPath.isNotNull();
				}
				break;
			case IS_NOT_NULL:
				if(Boolean.TRUE.equals(val)) {
					predicate = finalPath.isNotNull();
				}else if(Boolean.FALSE.equals(val)) {
					predicate = finalPath.isNull();
				}
				break;
			case IS_EMPTY:
				predicate = builder.lessThan(builder.size((Path<Collection<Object>>)finalPath),1);
				break;
			case IS_NOT_EMPTY:
				predicate = builder.greaterThan(builder.size((Path<Collection<Object>>)finalPath),0);
				break;
			case CONTAINS:
				if(!values.isEmpty()) {
					List<Predicate> predicates = new ArrayList<>();
					if(collectionsAttribute ) {
						predicates.addAll(values.stream().map(v->builder.equal(finalPath, v)).collect(Collectors.toList()));
					}else {
						predicates.addAll(values.stream().map(v->builder.isMember(v, (Path<Collection<Object>>)finalPath)).collect(Collectors.toList()));
					}
					if(!predicates.isEmpty()) {
						predicate = builder.and(predicates.toArray(new Predicate[predicates.size()]));
					}
				}
				break;
			default:
				break;
		}
		if(CollectionUtils.isNotEmpty(infos.paramInfoLst)) {
			List<Predicate> predicatelst = new ArrayList<>(infos.paramInfoLst.size()+1);
			if(predicate!=null)
				predicatelst.add(predicate);
			for(int i=0;i<infos.paramInfoLst.size();i++) {
				predicatelst.add( genPredicateByInfos(infos.paramInfoLst.get(i), root, query, builder, paPathMap));
			}
			Predicate[] predicates = predicatelst.toArray(new Predicate[predicatelst.size()]);
			return infos.queryType==EntityQueryParamType.AND?builder.and(predicates):builder.or(predicates);
		}
		return predicate;
	}
	/**
	 * 根据查询条件生成jpa查询描述.
	 *
	 * @param <T> the generic type
	 * @param params the params
	 * @param clz the clz
	 * @return the specification
	 */
	public static <T> Specification<T> genPredicate(Object params, Class<? extends T> clz) {
		logger.info("generate predicate for class {}", clz.getName());
		List<EntityQueryParamInfo> infoLst = getQueryParamsInfos(params);
		return genPredicateByInfos(infoLst);
	}
	
	
	private static List<EntityQueryParamInfo> getQueryParamsInfos(Object params){
		if (params == null)
			return Collections.emptyList();

		List<EntityQueryParamInfo> infoLst = new ArrayList<>();
		Class<? extends Object> pclz = params.getClass();
		/*
		 * 筛选查询条件对象具有的属性
		 */
		List<Field> fields = Arrays.stream(FieldUtils.getAllFields(pclz)).filter(f -> {
			String getterName = "get" + f.getName().substring(0, 1).toUpperCase() + f.getName().substring(1);
			Method getter = org.apache.commons.lang3.reflect.MethodUtils.getAccessibleMethod(pclz, getterName);
			return getter!=null;
		}).collect(Collectors.toList());
		for (Field field : fields) {
			EntityQueryParamInfo queryParamInfo = genQueryParamInfo(field, params);
			if(queryParamInfo!=null) {
				infoLst.add(queryParamInfo);
				
			}
		}
		return infoLst;
	}
	private static EntityQueryParamInfo genQueryParamInfo(Field field, Object params) {
		/*
		 * 获取查询条件对象的属性的匹配方式
		 */
		String target = field.getName();
		EntityQueryParamType type = EntityQueryParamType.EQUAL;
		EntityQueryParam anno = field.getAnnotation(EntityQueryParam.class);
		if (anno != null) {
			if (StringUtils.isNotEmpty(anno.field())) {
				target = anno.field();
			}else if (StringUtils.isNotEmpty(anno.value())) {
				target = anno.value();
			}
			if (anno.type() != null) {
				type = anno.type();
			}
		}
		/*
		 * 获取属性值
		 */
		String getterName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
		Class<?> pclz = params.getClass();
		Method getter = org.apache.commons.lang3.reflect.MethodUtils.getAccessibleMethod(pclz, getterName);
		if (getter == null) {
			logger.info("{} dose not have getter method for field: {}", pclz.getName(), field.getName());
			return null;
		}
		Object val = null;
		try {
			val = getter.invoke(params);
		} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
			logger.info("get {}'s  field: {}'s value failed", pclz.getName(), field.getName());
		}
		if (val == null && !type.isValueNullable())
			return null;
		boolean valIsNotEmptyStr=!"".equals(val);
		Collection<?> values= null;
		if(val instanceof Collection) {
			values = (Collection<?>) val;
		}else if(val instanceof Object[]) {
			values = Arrays.asList((Object[])val);
		}else {
			values= Arrays.asList(val);
		}
		return new EntityQueryParamInfo(field.getName(), val, target, type,valIsNotEmptyStr,values, null);
	}
	private static List<EntityQueryParamInfo> getQueryParamsInfos(List<EntityQueryCondition> conditions, Class<?> clz){
		if (conditions == null)
			return Collections.emptyList();
		return conditions.stream()
				.map(condition->{
					EntityQueryParamType type = condition.getQueryType()!=null?condition.getQueryType():EntityQueryParamType.EQUAL;
					Object val = condition.getValue();
					if(val == null && !type.isValueNullable()) {
						return null;
					}
					boolean valIsNotEmptyStr=!"".equals(val);
					Collection<?> values= null;
					if(val instanceof Collection) {
						values = (Collection<?>) val;
					}else if(val instanceof Object[]) {
						values = Arrays.asList((Object[])val);
					}else {
						values= Arrays.asList(val);
					}
					List<EntityQueryParamInfo> paramInfoLst =null;
					if(CollectionUtils.isNotEmpty(condition.getConditions())) {
						paramInfoLst = new ArrayList<>();
						paramInfoLst.addAll(getQueryParamsInfos(condition.getConditions(), clz));
					}
					return new EntityQueryParamInfo(condition.getProperty(), val, condition.getProperty(), type,valIsNotEmptyStr, values, paramInfoLst);
				})
				.filter(Objects::nonNull)
				.collect(Collectors.toList());
	}
}
class EntityQueryParamInfo{
	
	String name;
	Object value;
	String target;
	EntityQueryParamType queryType;
	boolean emptyStr;
	List<EntityQueryParamInfo> paramInfoLst;
	Collection<?> values;
	
	public EntityQueryParamInfo(String name
			, Object value
			, String target
			, EntityQueryParamType queryType
			, boolean emptyStr
			, Collection<?> values
			, List<EntityQueryParamInfo> paramInfoLst ) {
		super();
		this.name = name;
		this.value = value;
		this.target = target;
		this.queryType = queryType;
		this.emptyStr = emptyStr;
		this.values = values;
		this.paramInfoLst = paramInfoLst;
	}
	
}
